﻿using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;

namespace PacketRush.Core
{
    /// <summary>
    /// Represents a connection implementation used by <see cref="PacketRush.Core.DefaultClientNetworkingProvider"/> and
    /// <see cref="PacketRush.Core.DefaultServerNetworkingProvider"/>.
    /// </summary>
    public class DefaultConnection : INetConnection
    {
        /// <summary>
        /// Used to assign new connection ids by incrementing the value
        /// for each new connection.
        /// </summary>
        private static int _IdCounter = 1;

        /// <summary>
        /// Used to temporarily store socket error codes during socket operations.
        /// </summary>
        private SocketError _SocketSendError;

        /// <summary>
        /// Keeps a queue of pending outgoing packets that could not be delivered due
        /// to a wouldblock exception
        /// 
        /// TODO: Make sure this does not flood
        /// </summary>
        private Queue<byte[]> _PendingOutgoingPackets = new Queue<byte[]>(16);

        private int _NumberOfBytesReceived = 0;
        private int _NumberOfBytesExpected = 0;
        private byte[] _BytesReceived;

        /// <summary>
        /// The round-trip latency of this user in milliseconds.
        /// </summary>
        public int Latency { get; set; }

        /// <summary>
        /// The unique id of this connection.
        /// </summary>
        public int Id { get; set; }

        /// <summary>
        /// The remote end point of which this connection is connected to.
        /// </summary>
        public EndPoint RemoteEndPoint
        {
            get
            {
                return Socket.RemoteEndPoint;
            }
        }

        /// <summary>
        /// True if this connection is currently connected.
        /// </summary>
        public bool Connected
        {
            get
            {
                if (Socket == null) return false;
                return Socket.Connected;
            }
        }

        /// <summary>
        /// The TCP Socket associated to this connection.
        /// </summary>
        public Socket Socket { get; private set; }

        /// <summary>
        /// Called when the connection receives packets during a <see cref="PacketRush.Core.DefaultConnection.Receive"/>.
        /// </summary>
        public Action<byte[], INetConnection> ReceivePacketAction { get; private set; }
        /// <summary>
        /// Disconnect this connection
        /// </summary>
        public void Disconnect()
        {
            Socket.Shutdown(SocketShutdown.Both);
            Socket.Close();
            Socket = null;
        }

        /// <summary>
        /// Create a <c>DefaultConnection</c> instance.
        /// </summary>
        /// <param name="tcpSocket">A connected TCP socket.</param>
        /// <param name="receivePacketAction">A delegate that will be called when the connection receives packets during a receive call.</param>
        public DefaultConnection(Socket tcpSocket, Action<byte[], INetConnection> receivePacketAction)
        {
            // Only allow connected sockets to be associated
            if (!tcpSocket.Connected) throw new InvalidProgramException("the TCP Socket must be connected before associating it to a NetConnection");

            // Make this non blocking
            tcpSocket.Blocking = false;

            Socket = tcpSocket;
            ReceivePacketAction = receivePacketAction;
        }

        /// <summary>
        /// Send a packet to this connection using TCP.
        /// </summary>
        /// <param name="packet">The packet to send.</param>
        public void Send(byte[] packet)
        {
            if (SendPending())
            {
                Socket.Send(packet, 0, packet.Length, SocketFlags.None, out _SocketSendError);

                if (_SocketSendError == SocketError.Success) return;

                //
                // If the send operation blocks then we need to buffer the unsent packets
                // We make a copy to ensure the data is not changed by any other code that
                // has a reference to the raw packet
                //
                if (_SocketSendError == SocketError.WouldBlock)
                {
                    byte[] pendingPacket = new byte[packet.Length];
                    Buffer.BlockCopy(packet, 0, pendingPacket, 0, packet.Length);
                    _PendingOutgoingPackets.Enqueue(pendingPacket);
                }
                else
                {
                    throw new SocketException((int)_SocketSendError);
                }
            }
            else
            {
                _PendingOutgoingPackets.Enqueue(packet);
            }
        }

        /// <summary>
        /// Send pending packets.
        /// </summary>
        /// <returns>Returns true if there are no packets pending.</returns>
        public bool SendPending()
        {
            while (_PendingOutgoingPackets.Count > 0)
            {
                byte[] packet = _PendingOutgoingPackets.Peek();

                Socket.Send(packet, 0, packet.Length, SocketFlags.None, out _SocketSendError);

                if (_SocketSendError == SocketError.Success)
                {
                    _PendingOutgoingPackets.Dequeue();
                    continue;
                }

                if (_SocketSendError == SocketError.WouldBlock)
                {
                    return false;
                }
                else
                {
                    throw new SocketException((int)_SocketSendError);
                }
            }

            return true;
        }

        /// <summary>
        /// Receive data from this connection using TCP.
        /// </summary>
        public void Receive()
        {
            int bytesAvailable = Socket.Available;

            // Do we got stuff?
            if (bytesAvailable > 0)
            {
                if (_BytesReceived == null) _BytesReceived = new byte[bytesAvailable];

                // If we are expecting more than we can cater for then make some more room
                if (_BytesReceived.Length < bytesAvailable + _BytesReceived.Length)
                {
                    Array.Resize<byte>(ref _BytesReceived, bytesAvailable + _BytesReceived.Length);
                }

                // Receive the data from the socket
                _NumberOfBytesReceived += Socket.Receive(_BytesReceived, _NumberOfBytesReceived, bytesAvailable, SocketFlags.None);

                // Can we determine the expected number of bytes yet?
                if (_NumberOfBytesExpected == 0 && _NumberOfBytesReceived > 3)
                {
                    _NumberOfBytesExpected = BitConverter.ToInt32(_BytesReceived, 0);
                }

                // We still do not know how many bytes to expect, so lets leave it at that
                if (_NumberOfBytesExpected == 0) return;

                // Something dodgy is about to happen
                int received = _NumberOfBytesReceived;
                int read = 0;
                int expected = _NumberOfBytesExpected;

                while (true)
                {
                    if (received < expected)
                    {
                        byte[] remainingBytes = new byte[received];
                        Buffer.BlockCopy(_BytesReceived, read, remainingBytes, 0, received);
                        _NumberOfBytesExpected = 0;
                        _NumberOfBytesReceived = received;
                        _BytesReceived = remainingBytes;
                        break;
                    }

                    byte[] rawPacket = new byte[expected];
                    Buffer.BlockCopy(_BytesReceived, read, rawPacket, 0, rawPacket.Length);

                    // Hand this over to the packet consumer
                    ReceivePacketAction(rawPacket, this);

                    read += rawPacket.Length;
                    received -= rawPacket.Length;

                    if (received > 3) expected = BitConverter.ToInt32(_BytesReceived, read);
                }
            }
        }

        /// <summary>
        /// Assigns a new connection id to this connection.
        /// </summary>
        public void AssignNewId()
        {
            Id = _IdCounter++;
        }


        private uint _PacketIdCounter = 1;

        /// <summary>
        /// Returns a sequence index for a packet destined for this connection.
        /// </summary>
        /// <remarks>Returns an increasingly incremementing sequential number starting from 1.</remarks>
        /// <returns>An incrementing number starting from 1.</returns>
        public uint CreateNextPacketSequenceIndex()
        {
            return _PacketIdCounter++;
        }

        /// <summary>
        /// The local end point.
        /// </summary>
        public EndPoint LocalEndPoint
        {
            get
            {
                return Socket.LocalEndPoint;
            }
        }

    }
}
